﻿Imports System.ComponentModel
Imports System.Net
Imports System.Net.Sockets
Imports System.Web


Public Class CommentServer
    Implements IDisposable

#Region "Events"
    Public Event CommentReceived As EventHandler(Of CommentEventArgs)
    Protected Overridable Sub OnCommentReceived(ByVal e As CommentEventArgs)
        RaiseEvent CommentReceived(Me, e)
    End Sub

    Public Event ConnectionStatusChanged As EventHandler(Of ConnectionEventArgs)
    Protected Overridable Sub OnConnectionStatusChanged(ByVal e As ConnectionEventArgs)
        _status = e.Status
        RaiseEvent ConnectionStatusChanged(Me, e)
    End Sub

    Public Event ConfiguringWordResponseReceived As EventHandler(Of ConfiguringWordResponseEventArgs)
    Protected Overridable Sub OnConfiguringWordResponseReceived(ByVal e As ConfiguringWordResponseEventArgs)
        RaiseEvent ConfiguringWordResponseReceived(Me, e)
    End Sub

    Public Event PlayerStatusReceived As EventHandler(Of PlayerStatusEventArgs)
    Protected Overridable Sub OnPlayerStatusReceived(ByVal e As PlayerStatusEventArgs)
        RaiseEvent PlayerStatusReceived(Me, e)
    End Sub

#End Region

#Region "Public Properties"
    Private _isConnected As Boolean
    Public ReadOnly Property IsConnected() As Boolean
        Get
            Return _status = ConnectionStatus.Connected
        End Get
    End Property

    Private _cookieContainer As CookieContainer
    Public ReadOnly Property CookieContainer() As CookieContainer
        Get
            Return _cookieContainer
        End Get
    End Property

    Private _cookieSource As CookieSource
    Public Property CookieSource() As CookieSource
        Get
            Return _cookieSource
        End Get
        Set(ByVal value As CookieSource)
            If Not Status = ConnectionStatus.Connecting AndAlso _
               Not Status = ConnectionStatus.Connected Then
                _cookieSource = value
            End If
        End Set
    End Property

    Private _status As ConnectionStatus = ConnectionStatus.Disconnected
    Public ReadOnly Property Status() As ConnectionStatus
        Get
            Return _status
        End Get
    End Property

    Private _liveId As String = ""
    Public ReadOnly Property LiveId() As String
        Get
            Return _liveId
        End Get
    End Property

#End Region

#Region "Protected Properties"

    Private ThreadSyncObject As New Object
    Private _thread As String
    Protected Property Thread() As String
        Get
            SyncLock ThreadSyncObject
                Return _thread
            End SyncLock
        End Get
        Set(ByVal value As String)
            SyncLock ThreadSyncObject
                _thread = value
            End SyncLock
        End Set
    End Property

    Private PortSyncObject As New Object
    Private _port As Integer
    Protected Property Port() As Integer
        Get
            SyncLock PortSyncObject
                Return _port
            End SyncLock
        End Get
        Set(ByVal value As Integer)
            SyncLock PortSyncObject
                _port = value
            End SyncLock
        End Set
    End Property

    Private SyncHostNameObject As New Object
    Private _hostName As String
    Protected Property HostName() As String
        Get
            SyncLock SyncHostNameObject
                Return _hostName
            End SyncLock
        End Get
        Set(ByVal value As String)
            SyncLock SyncHostNameObject
                _hostName = value
            End SyncLock
        End Set
    End Property

    Private SyncStartTimeObject As New Object
    Private _startTime As Integer
    Protected Property StartTime() As Integer
        Get
            SyncLock SyncStartTimeObject
                Return _startTime
            End SyncLock
        End Get
        Set(ByVal value As Integer)
            SyncLock SyncStartTimeObject
                _startTime = value
            End SyncLock
        End Set
    End Property

    Private SyncUserIdObject As New Object
    Private _userId As String
    Protected Property UserId() As String
        Get
            SyncLock SyncUserIdObject
                Return _userId
            End SyncLock
        End Get
        Set(ByVal value As String)
            SyncLock SyncUserIdObject
                _userId = value
            End SyncLock
        End Set
    End Property
#End Region


    Private WithEvents Worker As BackgroundWorker

    ''' <summary>
    ''' コメント取得のための接続
    ''' </summary>
    ''' <param name="lv">live ID</param>
    ''' <remarks></remarks>
    Public Sub ConnectAsync(ByVal liveId As String, ByVal cookieSource As CookieSource)

        If Me.Status <> ConnectionStatus.Disconnected Then
            Exit Sub
        End If

        _liveId = liveId
        Me.CookieSource = cookieSource

        Worker = New BackgroundWorker
        Worker.WorkerSupportsCancellation = True
        Worker.WorkerReportsProgress = True
        Worker.RunWorkerAsync(liveId)

    End Sub

    Public Sub DisconnectAsync()

        If Me.Status = ConnectionStatus.Disconnected OrElse _
            Me.Status = ConnectionStatus.Disconnecting Then
            Exit Sub
        End If

        Try
            Worker.ReportProgress(0, ConnectionStatus.Disconnecting)
            If Client Is Nothing AndAlso Client.Connected Then
                Client.Close()
            End If
            Worker.CancelAsync()
        Catch ex As Exception
            Worker.ReportProgress(0, ConnectionStatus.Disconnected)
        End Try

    End Sub

    Private NgWord As XDocument

    Private Function GetConfigurengWords(ByVal lv As String) As XDocument

        Dim content = GetContent(New Uri(String.Format(My.Resources.ConfigurengWordUriFormat, lv)))
        If content = "" Then
            Return Nothing
        End If

        Try
            Return XDocument.Parse(content)
        Catch ex As Exception
            Return Nothing
        End Try

        '<response_ngword status="fail">
        '  <error>
        '    <code>PARAMERROR</code>
        '    <description>パラメータが不正です</description>
        '    <code>NOTLOGIN</code> 
        '    <description>ログインしていません</description> 
        '  </error>
        '</response_ngword>

    End Function

    Private Sub Woker_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles Worker.DoWork
        Dim lv = CStr(e.Argument)

        Worker.ReportProgress(0, ConnectionStatus.Connecting)

        ' Cookie 取得
        Dim nicoLibrary = New NicoLibrary.NicoCookieCollector
        _cookieContainer = nicoLibrary.GetCookie(DirectCast(Me.CookieSource, NicoLibrary.CookieType), "user_session")
        If nicoLibrary.StatusMsg <> "" Then
            ' TODO
            Exit Sub
        End If

        ' NGワード取得
        NgWord = GetConfigurengWords(lv)
        If NgWord Is Nothing Then
            Exit Sub
        End If

        Dim response = New ConfiguringWordResponse(NgWord)
        Worker.ReportProgress(0, response)
        If response.Status = "fail" Then
            Exit Sub
        End If

        ' Player Status 取得
        Dim statusContent = GetContent(New Uri(String.Format(My.Resources.PlayerStatusUriFormat, lv)))
        If statusContent = "" Then
            Exit Sub
        End If

        Dim statusXml As XDocument
        Try
            statusXml = XDocument.Parse(statusContent)

            '<getplayerstatus status="fail" time="1244222805">
            '  <error>
            '    <code>closed</code>
            '    <code>full</code>
            '  </error>
            '</getplayerstatus>

            Dim status = New PlayerStatus(statusXml)
            Worker.ReportProgress(0, status)
            If status.Status = "fail" Then
                Exit Sub
            End If
        Catch ex As Exception
            Exit Sub
        End Try

        ' 送信に必要な情報
        Dim startTime = statusXml.<getplayerstatus>.<stream>.<start_time>.Value
        If startTime Is Nothing OrElse Not Integer.TryParse(startTime, Me.StartTime) Then
            Exit Sub
        End If

        UserId = statusXml.<getplayerstatus>.<user>.<user_id>.Value
        If UserId Is Nothing Then
            Exit Sub
        End If

        ' TODO Premium

        ' 接続
        Try
            HostName = statusXml.<getplayerstatus>.<ms>.<addr>.Value
            If Not Integer.TryParse(statusXml.<getplayerstatus>.<ms>.<port>.Value, Port) Then
                ' TODO
                Exit Sub
            End If
            Thread = statusXml.<getplayerstatus>.<ms>.<thread>.Value

            CommentGettingProcedure(HostName, Port, Thread)

        Catch ex As Exception
            ' TODO
        End Try
    End Sub

    Private Sub Woker_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles Worker.ProgressChanged

        If TypeOf e.UserState Is Comment Then
            Dim comment = DirectCast(e.UserState, Comment)
            OnCommentReceived(New CommentEventArgs(comment))

        ElseIf TypeOf e.UserState Is ConnectionStatus Then
            Dim status = DirectCast(e.UserState, ConnectionStatus)
            OnConnectionStatusChanged(New ConnectionEventArgs(status))

        ElseIf TypeOf e.UserState Is ConfiguringWordResponse Then
            Dim response = DirectCast(e.UserState, ConfiguringWordResponse)
            OnConfiguringWordResponseReceived(New ConfiguringWordResponseEventArgs(response))

        ElseIf TypeOf e.UserState Is PlayerStatus Then
            Dim status = DirectCast(e.UserState, PlayerStatus)
            OnPlayerStatusReceived(New PlayerStatusEventArgs(status))

        End If

    End Sub

    Private Sub Woker_RunWorkerCompleted(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles Worker.RunWorkerCompleted
        OnConnectionStatusChanged(New ConnectionEventArgs(ConnectionStatus.Disconnected))
        Worker = Nothing
    End Sub

    Private Client As TcpClient

    Private Sub CommentGettingProcedure(ByVal hostName As String, ByVal port As Integer, ByVal thread As String)

        Dim tag = String.Format(My.Resources.ThreadFormat, thread, -10) & vbNullChar
        Dim writeBuffer = System.Text.Encoding.UTF8.GetBytes(tag)

        Client = New TcpClient(hostName, port)
        Using stream = Client.GetStream
            stream.Write(writeBuffer, 0, writeBuffer.Length)

            Dim length As Integer
            Dim message As String = ""
            Dim bytes = New List(Of Byte)

            Worker.ReportProgress(0, ConnectionStatus.Connected)

            Do
                If Worker.CancellationPending Then
                    Exit Do
                End If

                Dim buffer = New Byte(4096 - 1) {}
                length = stream.Read(buffer, 0, buffer.Length)
                If length < 1 Then
                    Exit Do
                End If

                For i = 0 To length - 1
                    If buffer(i) = 0 Then
                        message = System.Text.Encoding.UTF8.GetString(bytes.ToArray)
                        Dim text = CommentParsingProcedure(message)

                        If text = "/disconnect" Then
                            Exit Do
                        End If

                        bytes.Clear()
                        Continue For
                    End If
                    bytes.Add(buffer(i))
                Next


                If length < 4096 - 1 Then
                    message = ""
                End If
            Loop

        End Using

    End Sub

    Private Function CommentParsingProcedure(ByVal text As String) As String

        Try
            If text = "" Then
                Return ""
            End If

            Dim xml = XElement.Parse(text)
            If xml.Name = "chat" Then

                Dim comment = New Comment With { _
                    .Text = System.Web.HttpUtility.HtmlDecode(xml.Value), _
                    .No = CInt(xml.@no), _
                    .Premium = CInt(xml.@premium), _
                    .Thread = xml.@thread, _
                    .UserId = xml.@user_id, _
                    .Date = New DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(CInt(xml.@date)).ToLocalTime}

                If xml.@anonymity = "1" Then
                    comment.IsAnonymity = True
                Else
                    comment.IsAnonymity = False
                End If

                For Each e In NgWord...<ngclient>

                    If e.<type>.Value = "word" AndAlso _
                        System.Text.RegularExpressions.Regex.IsMatch(comment.Text, e.<source>.Value) Then
                        comment.IsNG = True
                        Exit For
                    End If
                Next

                Worker.ReportProgress(0, comment)
                Return comment.Text
            End If

        Catch ex As Exception

        End Try
        Return ""


    End Function


    Public Sub SendAsync(ByVal text As String)

        If Not IsConnected Then
            Exit Sub
        End If

        Dim worker = New BackgroundWorker
        AddHandler worker.DoWork, AddressOf CommentSendingProcedure
        worker.RunWorkerAsync(text)
    End Sub

    Private Sub CommentSendingProcedure(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs)
        Dim text = CStr(e.Argument)
        Dim chat = New With {.thread = "", .ticket = "", .vpos = "", .postkey = "", .mail = " 184", .user_id = "", .premium = ""}

        Dim uri = New Uri(String.Format(My.Resources.GetPostKeyUriFormat, Me.Thread, 0))
        Dim messageBytes = System.Text.Encoding.ASCII.GetBytes("")

        Dim request = DirectCast(WebRequest.Create(uri), HttpWebRequest)
        request.CookieContainer = Me.CookieContainer
        request.Method = "POST"
        request.ContentType = "application/x-www-form-urlencoded"
        request.ContentLength = messageBytes.Length

        Using stream = request.GetRequestStream
            stream.Write(messageBytes, 0, messageBytes.Length)
        End Using

        Using response = DirectCast(request.GetResponse, HttpWebResponse), _
              reader = New System.IO.StreamReader(response.GetResponseStream)

            Dim body = reader.ReadToEnd
            Dim match = System.Text.RegularExpressions.Regex.Match(body, "postkey=(?<value>[^=]+)")
            If match.Success Then
                chat.postkey = match.Groups("value").Value
            End If

        End Using



        Using client = New TcpClient(HostName, Port), _
              stream = client.GetStream

            Dim tag = String.Format(My.Resources.ThreadFormat, Thread, -1) & vbNullChar
            Dim writeBuffer = System.Text.Encoding.UTF8.GetBytes(tag)
            stream.Write(writeBuffer, 0, writeBuffer.Length)

            Dim readBuffer = New Byte(1023) {}
            stream.Read(readBuffer, 0, readBuffer.Length)

            Dim input = System.Text.Encoding.UTF8.GetString(readBuffer)
            Dim match As System.Text.RegularExpressions.Match
            match = System.Text.RegularExpressions.Regex.Match(input, "thread=""(?<value>[^=]+)""")
            If Not match.Success Then
                Exit Sub
                ' TODO
            End If
            chat.thread = match.Groups("value").Value

            match = System.Text.RegularExpressions.Regex.Match(input, "ticket=""(?<value>[^=]+)""")
            If Not match.Success Then
                Exit Sub
                ' TODO
            End If
            chat.ticket = match.Groups("value").Value

            match = System.Text.RegularExpressions.Regex.Match(input, "server_time=""(?<value>[^=]+)""")
            If Not match.Success Then
                Exit Sub
                ' TODO
            End If
            Dim serverTime = match.Groups("value").Value

            chat.vpos = CStr((CInt(serverTime) - Me.StartTime) * 100)

        End Using

        Using client = New TcpClient(HostName, Port), _
              stream = client.GetStream

            Dim tag As String
            With chat
                tag = String.Format("<chat thread=""{1}"" ticket=""{2}"" vpos=""{3}"" postkey=""{4}"" mail=""{5}"" user_id=""{6}"" premium=""{7}"">{0}</chat>" & vbNullChar, _
                                     text, .thread, .ticket, .vpos, .postkey, .mail, UserId, "1")
            End With

            Dim writeBuffer = System.Text.Encoding.UTF8.GetBytes(tag)
            stream.Write(writeBuffer, 0, writeBuffer.Length)

            Dim readBuffer = New Byte(1023) {}
            stream.Read(readBuffer, 0, readBuffer.Length)

        End Using

    End Sub

#Region "Broadcast"

    Public Sub SendBroadcastComment(ByVal text As String)

        Dim body = GetContent(New Uri(String.Format("http://live.nicovideo.jp/api/broadcast/{0}?body={1}&mail={2}", _
                                                     LiveId, HttpUtility.UrlEncode(text), "")))


        'Dim flag2 As Boolean
        'Try
        '    str3 = HttpUtility.UrlEncode(data1.Comment)
        '    location = Me.WrittenComment.Location
        '    If (Me.WrittenComment.Size <> String.Empty) Then
        '        location = (location & (" " & Me.WrittenComment.Size))
        '    End If
        '    If (Me.WrittenComment.Color <> String.Empty) Then
        '        location = (location & (" " & Me.WrittenComment.Color))
        '    End If
        '    request = TryCast(WebRequest.Create((((Me.API_Broadcast_URL & "/lv" & Me._LiveNum) & "?body=" & str3) & "&mail=" & location)), HttpWebRequest)
        '    request.CookieContainer = Me._NicoCC
        '    response = TryCast(request.GetResponse, HttpWebResponse)
        '    reader = New StreamReader(response.GetResponseStream, Encoding.UTF8)
        '    If (reader.ReadToEnd.Replace("status=", "") <> "ok") Then
        '        Dim num5 As Integer = 0
        '        <Module>._CxxThrowException(DirectCast(AddressOf num5, Void*), AddressOf <Module>._TI1H)
        '    End If
        '    Return True

    End Sub

#End Region

    Protected Function GetContent(ByVal uri As Uri) As String

        Dim request = DirectCast(WebRequest.Create(uri), HttpWebRequest)
        request.CookieContainer = Me.CookieContainer

        Dim content As String = ""
        Try
            Dim response = DirectCast(request.GetResponse, HttpWebResponse)
            Using stream = response.GetResponseStream, _
                  reader = New System.IO.StreamReader(stream, System.Text.Encoding.UTF8)
                content = reader.ReadToEnd
            End Using

        Catch webEx As WebException
            ' TODO
        Catch ex As Exception
            ' TODO
        End Try

        Return content
    End Function

#Region "Dispose"
    Private disposedValue As Boolean = False        ' 重複する呼び出しを検出するには

    ' IDisposable
    Protected Overridable Sub Dispose(ByVal disposing As Boolean)
        If Not Me.disposedValue Then
            If disposing Then
                ' TODO: 他の状態を解放します (マネージ オブジェクト)。

            End If

            ' TODO: ユーザー独自の状態を解放します (アンマネージ オブジェクト)。
            ' TODO: 大きなフィールドを null に設定します。
        End If
        Me.disposedValue = True
    End Sub

#Region " IDisposable Support "
    ' このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
    Public Sub Dispose() Implements IDisposable.Dispose
        ' このコードを変更しないでください。クリーンアップ コードを上の Dispose(ByVal disposing As Boolean) に記述します。
        Dispose(True)
        GC.SuppressFinalize(Me)
    End Sub
#End Region

#End Region

End Class
